import { Dictionary } from '@shencom/typing';
import { paginate } from 'nestjs-typeorm-paginate';
import { camelCase } from 'scule';
import {
  FindOptionsOrder,
  FindOptionsRelations,
  ObjectLiteral,
  Repository,
  SelectQueryBuilder,
} from 'typeorm';

import { IndexDto } from '../dto/index/index.dto';
import { IndexSorts } from '../types';
import { flatQueryDto } from '../utils/index.helper';

function hasColumn(repository: Repository<any>, propertyKey: string) {
  return repository.metadata.columns.some(
    (v) => v.propertyName === propertyKey,
  );
}

interface IndexOption<T extends ObjectLiteral> {
  repository: Repository<T>;
  queryBuilder: SelectQueryBuilder<T>;
}

interface IndexSortOption<T extends ObjectLiteral> extends IndexOption<T> {
  sorts?: IndexDto['sorts'];
}
type SortKeyVal<T extends string = string> = Record<T, IndexSorts['orderType']>;

export function repositoryIndexSort<T extends ObjectLiteral>(
  options: IndexSortOption<T>,
) {
  const { repository, queryBuilder } = options;

  const inside: SortKeyVal = {};

  const name = repository.metadata.name;

  const sorts = options.sorts || [];

  const data = sorts.reduce<SortKeyVal>((acc, cur) => {
    if (hasColumn(repository, camelCase(cur.orderField))) {
      acc[`${name}.${cur.orderField}`] = cur.orderType;
    }
    return acc;
  }, Object.create(null)) as FindOptionsOrder<T>;

  if (hasColumn(repository, 'createdAt')) {
    if (!('createdAt' in data)) {
      inside[`${name}.createdAt`] = 'DESC';
    }
  }

  const order = { ...data, ...inside };

  Object.keys(order).forEach((key, i) => {
    if (i === 0) {
      queryBuilder.orderBy(key, order[key]);
    } else {
      queryBuilder.addOrderBy(key, order[key]);
    }
  });

  return queryBuilder;
}

interface IndexQueryOption<T extends ObjectLiteral> extends IndexOption<T> {
  query?: IndexDto['query'];
}

export function repositoryIndexQuery<T extends ObjectLiteral>(
  options: IndexQueryOption<T>,
) {
  const { repository, queryBuilder, query } = options;

  const q = flatQueryDto(query || []);

  q.forEach((v, i) => {
    if (!v.prop) return;
    if (!hasColumn(repository, v.prop)) return;

    if (v.value === undefined) return;

    const name = repository.metadata.name;

    const prop = `${name}.${v.prop}`;
    const operate = v.operate?.toLocaleUpperCase();
    let where: string;
    let parameters: Dictionary = {};

    if (operate === 'BTW') {
      // { label: '区间', value: 'BTW' },
      const [start, end] = v.value.split(',');
      where = `${prop} BETWEEN :${prop}_start AND :${prop}_end`;
      parameters = { [`${prop}_start`]: start, [`${prop}_end`]: end };
    } else if (operate === 'IN') {
      // { label: '包括', value: 'IN' },
      where = `${prop} IN (:...${prop})`;

      const values =
        typeof v.value === 'string'
          ? v.value.split(',')
          : Array.isArray(v.value)
            ? v.value
            : [v.value];

      parameters = { [`${prop}`]: values };
    } else if (operate === 'EQ') {
      // { label: '=', value: 'EQ' },
      where = `${prop} = :${prop}`;
      parameters = { [`${prop}`]: v.value };
    } else if (operate === 'NEQ') {
      // { label: '≠', value: 'NEQ' },
      where = `${prop} != :${prop}`;
      parameters = { [`${prop}`]: v.value };
    } else if (operate === 'NULL') {
      // { label: '为空', value: 'NULL' },
      where = `${prop} IS NULL`;
      parameters = {};
    } else if (operate === 'NN') {
      // { label: '非空', value: 'NN' },
      where = `${prop} IS NOT NULL`;
      parameters = {};
    } else if (operate === 'LT') {
      // { label: '<', value: 'LT' },
      where = `${prop} < :${prop}`;
      parameters = { [`${prop}`]: v.value };
    } else if (operate === 'GT') {
      // { label: '>', value: 'GT' },
      where = `${prop} > :${prop}`;
      parameters = { [`${prop}`]: v.value };
    } else if (operate === 'LTE') {
      // { label: '≤', value: 'LTE' },
      where = `${prop} <= :${prop}`;
      parameters = { [`${prop}`]: v.value };
    } else if (operate === 'GTE') {
      // { label: '≥', value: 'GTE' },
      where = `${prop} >= :${prop}`;
      parameters = { [`${prop}`]: v.value };
    } else if (operate === 'ILIKE') {
      where = `${prop} ILIKE :${prop}`;
      parameters = { [`${prop}`]: `%${v.value}%` };
    } else if (operate === 'LIKE') {
      // { label: '模糊匹配', value: 'LIKE' },
      where = `${prop} LIKE :${prop}`;
      parameters = { [prop]: `%${v.value}%` };
    } else if (operate === 'LL') {
      // { label: '左模糊匹配', value: 'LL' },
      where = `${prop} LIKE :${prop}`;
      parameters = { [`${prop}`]: `%${v.value}` };
    } else if (operate === 'RL') {
      // { label: '右模糊匹配', value: 'RL' },
      where = `${prop} LIKE :${prop}`;
      parameters = { [`${prop}`]: `${v.value}%` };
    } else {
      return;
    }

    if (i === 0) {
      queryBuilder.where(where, parameters);
    } else if (v.lr === 'and') {
      queryBuilder.andWhere(where, parameters);
    } else {
      queryBuilder.orWhere(where, parameters);
    }
  });

  return queryBuilder;
}

interface Option<T extends ObjectLiteral, D> {
  dto: D;
  repository: Repository<T>;
  relation?: FindOptionsRelations<T> | string[];
}

export function indexBuilder<T extends ObjectLiteral, D extends IndexDto>(
  option: Option<T, D>,
) {
  const { dto, repository, relation } = option;
  const name = repository.metadata.name;

  let queryBuilder = repository.createQueryBuilder();

  queryBuilder = repositoryIndexSort({
    queryBuilder,
    sorts: dto.sorts,
    repository: repository,
  });

  queryBuilder = repositoryIndexQuery({
    queryBuilder,
    query: dto.query,
    repository: repository,
  });

  if (Array.isArray(relation)) {
    relation.forEach((item) => {
      queryBuilder = queryBuilder.leftJoinAndSelect(`${name}.${item}`, item);
    });
  }

  return queryBuilder;
}

export async function paginateBuilder<
  T extends ObjectLiteral,
  D extends IndexDto,
>(queryBuilder: SelectQueryBuilder<T>, dto: D) {
  const res = await paginate(queryBuilder, {
    page: dto.page ?? 1,
    limit: dto.size || 10,
  });
  return res;
}

export async function indexPaginateBuilder<
  T extends ObjectLiteral,
  D extends IndexDto,
>(option: Option<T, D>) {
  const { dto } = option;

  const queryBuilder = indexBuilder(option);

  const res = await paginateBuilder(queryBuilder, dto);

  return res;
}
